今天來實戰,將前三天學習到關於 coroutine 的基本概念和用法在我們的專案內實作!
因為我們學到的是用 launch 啟動 coroutine 來向網路請求資料,所以想先從這邊開始下手,將目前的程式碼修改為以 launch 啟動 :
這是目前失敗的程式碼 :
向網路請求資料
private suspend fun fetchCoffeeShopData(): Deferred<Response> {
    return CoroutineScope(Dispatchers.IO).async {
        // 創建一個 OkHttpClient 實例
        val client = OkHttpClient()
        // 設置要發送的 HTTP 請求
        val request = Request.Builder()
            .url("http://cafenomad.tw/api/v1.2/cafes/taipei")
            .build()
        // 使用 OkHttpClient 發送同步請求
        client.newCall(request).execute()
    }
}
呼叫 fetchCoffeeShopData()
binding.button.setOnClickListener {
    runBlocking {
        try {
            val response = fetchCoffeeShopData().await()
            // 在主線程更新 UI,顯示回應內容
            withContext(Dispatchers.Main) {
                // 檢查回應是否成功
                if (response.isSuccessful) {
                    val shops = response.body?.string()
                    // 這個 Scope 即是在 Android Main Thread 上執行,可以在這裡取得 Server 回傳的資料後更新 UI
                    binding.textView.text = shops
                }
                else {
                    // 處理請求失敗的情況
                    println("Request failed with code: ${response.code}")
                }
            }
        }
        catch (e: Exception) {
            println("Error: ${e.message}")
        }
    }
}
修改方法的定義,改為無回傳值 :
// 原先的程式碼 : 
private suspend fun fetchCoffeeShopData(): Deferred<Response> {}
// 修改後 : 
private suspend fun fetchCoffeeShopData() {}
使用 withContext()搭配 Dispatcher.IO 將目前的執行緒切換為工作執行緒 :
// 原先的程式碼
private suspend fun fetchCoffeeShopData(): Deferred<Response> {
		return CoroutineScope(Dispatchers.IO).async {
		}
}
// 修改後
private suspend fun fetchCoffeeShopData() {
    withContext(Dispatchers.IO) {
    }
}
自定義 Throwable CoffeeShopsRefreshError 類別來拋出例外 :
// 修改後
class CoffeeShopsRefreshError(message: String, cause: Throwable?) : Throwable(message, cause)
使用 try-catch 做例外處理,包住發送網路請求的程式碼 :
// 原先程式碼
// 使用 OkHttpClient 發送同步請求
client.newCall(request).execute()
// 修改後
val response = try {
    // 使用 OkHttpClient 發送同步請求
    client.newCall(request).execute()
}
catch (cause: Throwable) {
    throw CoffeeShopsRefreshError("Unable to refresh data", cause)
}
將回傳結果 response strirng 使用 LiveData 通知訂閱者(觀察者)來更新畫面
// 修改後
if (response.isSuccessful) {
    // 成功後通知畫面更新
    liveShops.postValue(response.body?.string())
}
else {
    throw CoffeeShopsRefreshError("Unable to refresh data", null)
}
來修改接收使用者點擊事件後需要執行的部分,這邊會先透過 Dispatcher.Main 讓 coroutine 在主執行緒被啟動,而且也要記得 suspend 函式需要在 coroutine 內被呼叫 :
// 原先程式碼
binding.button.setOnClickListener {
    runBlocking {
    }
}
// 修改後
binding.button.setOnClickListener {
    // 這個 Scope 即是在 Android Main Thread 上執行向網路取得咖啡廳資料
    CoroutineScope(Dispatchers.Main).launch {
    }
}
修改執行網路請求的方法
// 原先的程式碼
try {
    val response = fetchCoffeeShopData().await()
}
catch (e: Exception) {
    println("Error: ${e.message}")
}
這邊有多加 progressbar,在請求前會出現,當請求結束後會消失 :
try {
    binding.progressbar.visibility = View.VISIBLE
    fetchCoffeeShopData()
}
catch (e: CoffeeShopsRefreshError) {
    binding.progressbar.visibility = View.INVISIBLE
    binding.textView.text = "Request failed \nmessage: ${e.message}"
}
// 原先的程式碼
// 在主線程更新 UI,顯示回應內容
withContext(Dispatchers.Main) {
    // 檢查回應是否成功
    if (response.isSuccessful) {
        val shops = response.body?.string()
        // 這個 Scope 即是在 Android Main Thread 上執行,可以在這裡取得 Server 回傳的資料後更新 UI
        binding.textView.text = shops
    }
    else {
        // 處理請求失敗的情況
        println("Request failed with code: ${response.code}")
    }
}
這裡我將畫面更新的程式碼拉出來,因為是透過 launch 啟動 coroutine,表示我們需要特別處理回傳資料,可以使用 callback 也可以使用今天介紹的 LiveData。
LiveData 實作了觀察者模式,所以只要被觀察的對象資料有異動,就會通知觀察者,我們只需要實作觀察的方法,就可以直接在數據更新時刷新畫面。
// 修改後
private val liveShops = MutableLiveData<String?>()
// 更新畫面資料
liveShops.observe(this) { coffeeShops ->
    CoroutineScope(Dispatchers.Main).launch {
        binding.textView.text = coffeeShops
        binding.progressbar.visibility = View.INVISIBLE
    }
}
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val liveShops = MutableLiveData<String?>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // Hello Kotlin
        binding.textView.text = "Hello Kotlin"
        // 點擊按鈕後取得咖啡廳資料
        binding.button.setOnClickListener {
            // 這個 Scope 即是在 Android Main Thread 上執行向網路取得咖啡廳資料
            CoroutineScope(Dispatchers.Main).launch {
                try {
                    binding.progressbar.visibility = View.VISIBLE
                    fetchCoffeeShopData()
                }
                catch (e: CoffeeShopsRefreshError) {
                    binding.progressbar.visibility = View.INVISIBLE
                    binding.textView.text = "Request failed \nmessage: ${e.message}"
                }
            }
        }
        // 更新畫面資料
        liveShops.observe(this) { coffeeShops ->
            CoroutineScope(Dispatchers.Main).launch {
                binding.textView.text = coffeeShops
                binding.progressbar.visibility = View.INVISIBLE
            }
        }
    }
    private suspend fun fetchCoffeeShopData() {
        withContext(Dispatchers.IO) {
            // 創建一個 OkHttpClient 實例
            val client = OkHttpClient()
            // 設置要發送的 HTTP 請求
            val request = Request.Builder()
                .url("http://cafenomad.tw/api/v1.2/cafes/taipei")
                .build()
            val response = try {
                // 使用 OkHttpClient 發送同步請求
                client.newCall(request).execute()
            }
            catch (cause: Throwable) {
                throw CoffeeShopsRefreshError("Unable to refresh data", cause)
            }
            if (response.isSuccessful) {
                // 成功後通知畫面更新
                liveShops.postValue(response.body?.string())
            }
            else {
                throw CoffeeShopsRefreshError("Unable to refresh data", null)
            }
        }
    }
}
class CoffeeShopsRefreshError(message: String, cause: Throwable?) : Throwable(message, cause)
還在看要怎麼上傳影片,等我研究研究在更新上來!
今天使用 launch 改寫失敗的程式碼,雖然成功了,但並不是我理想中的樣子,因為更新畫面的部分並沒有想像中的直觀,就算不用 coroutine 也能達到差不多的效果。所以下一篇要來改用 async 啟動 coroutine,到時候再來看看是不是想要的結果吧~